/*
 * @author zz85 / https://github.com/zz85
 *
 * Ascii generation is based on http://www.nihilogic.dk/labs/jsascii/
 * Maybe more about this later with a blog post at http://lab4games.net/zz85/blog
 *
 * 16 April 2012 - @blurspline
 * 29 October 2013 - Shigerello
 */
/*
 * jsAscii 0.1 Copyright (c) 2008 Jacob Seidelin, jseidelin@nihilogic.dk,
 * http://blog.nihilogic.dk/ MIT License
 * [http://www.nihilogic.dk/licenses/mit-license.txt]
 */
/*
 * TODO: Currently, pixel reading is performed on an intermediate canvas,
 * where the image from the original canvas is drawn beforehand.
 * This is a workaround for WebGL canvas, and in truth, one can read pixels directly
 * from the original canvas without any intermediate; instead of performing pixel
 * reading directly on WebGLRenderingContext (readPixels), this library draws the
 * image from WebGLRenderingContext into CanvasRenderingContext2D first (drawImage),
 * then read pixels from this CanvasRenderingContext2D (getImageData). This is a
 * waste of time and memory resources.
 * Maybe, abstracting 'readPixels' and 'getImageData' with a common interface can
 * remove this inconvenience. ~~~ Shigerello, 29 October 2013
 * TODO: Initial option parsing and option parsing at setup() are very similar, merge
 * them. ~~~ Shigerello, 25 November 2013
 * TODO: Implement binary search "filling width with the appropriate numbers of chars"
 * for better visual experience in "canvas" render style.
 * ~~~ Shigerello, 25 November 2013
 * TODO: In "canvas" render style, cache the result of repeating process, like
 * fillText(). ~~~ Shigerello, 25 November 2013
 * TODO: If it's possible, because all quantized rectangular regions where chars or
 * blocks are rendered are independent, they should be processed in parallel.
 * ~~~ Shigerello, 25 November 2013
 */
THREE.AsciiEffect = function(parent, renderer, charSet, options) {

	// for easy access
	var self = this;

	// It's fun to create one your own!
	// Darker bolder character set from https://github.com/saw/Canvas-ASCII-Art/
	//
	// '.\'`^",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$'
	// ' .,:;=|iI+hHOE#`$';
	// ' .,:;i1tfLCG08@'
	// ' CGO08@'
	var aCharList = (charSet === undefined) ? ' .:-=+*#%@' : charSet;
	if (aCharList.length < 3) {
		console
				.log("AsciiEffect: error: character set must have 2 chars or more");
		return;
	}

	if (!options)
		options = {};

	// Some ASCII settings
	var iScale = !options['scale'] ? 1 : options['scale'];

	// nice but slows down rendering!
	var bColor = !options['color'] ? false : options['color'];

	// Transparency
	var bAlpha = !options['alpha'] ? false : options['alpha'];

	// blocked characters. like good O dos
	var bBlock = !options['block'] ? false : options['block'];

	// black is white, white is black
	var bInvert = !options['invert'] ? false : options['invert'];

	// Resolution
	var strResolution = !options['strResolution'] ? 'low'
			: options['strResolution'];
	var fResolution;
	switch (strResolution) {
	case "low":
		fResolution = 0.25;
		break;
	case "medium":
		fResolution = 0.5;
		break;
	case "high":
		fResolution = 1;
		break;
	}

	// adjust letter-spacing for all combinations of scale and resolution to get
	// it to fit within the image width.
	var fLetterSpacing = 0;
	if (strResolution == "low") {
		switch (iScale) {
		case 1:
			fLetterSpacing = -1;
			break;
		case 2:
		case 3:
			fLetterSpacing = -2.1;
			break;
		case 4:
			fLetterSpacing = -3.1;
			break;
		case 5:
			fLetterSpacing = -4.15;
			break;
		}
	} else if (strResolution == "medium") {
		switch (iScale) {
		case 1:
			fLetterSpacing = 0;
			break;
		case 2:
			fLetterSpacing = -1;
			break;
		case 3:
			fLetterSpacing = -1.04;
			break;
		case 4:
		case 5:
			fLetterSpacing = -2.1;
			break;
		}
	} else if (strResolution == "high") {
		switch (iScale) {
		case 1:
		case 2:
			fLetterSpacing = 0;
			break;
		case 3:
		case 4:
		case 5:
			fLetterSpacing = -1;
			break;
		}
	}

	// Rendering style
	var strAsciify = !options['renderStyle'] ? 'textual'
			: options['renderStyle'];
	var init;
	var asciify;
	switch (strAsciify) {
	case "textual":
		init = initDOM;
		asciify = asciifyDOM;
		break;
	case "canvas":
		init = initCanvas;
		asciify = asciifyCanvas;
		break;
	case "no_effect":
		init = function() {
		};
		asciify = function() {
		};
		break;
	}

	var fontColor = options['fontColor'] || 'black';
	var bgColor = options['bgColor'] || 'hsl(0, 0%, 70%)';

	// Do not change unless you know what you're doing; this code is
	// based on metrics of Courier font; changing font likely breaks
	// everything.
	// At least, font must be a monospaced one.
	var strFont = "Courier";

	// Original canvas size in px
	var width, height;

	// Quantized size
	var iWidth, iHeight;

	var domElement;

	var oOriginalCanvas = renderer.domElement;

	// Assume the bounding box of every character as a rectangle with aspect
	// ratio 0.5 (height : width = 0.5 : 1).
	var fFontHeight = Math.round(2 * iScale / fResolution);
	var fFontWidth = fFontHeight / 2;

	this.setSize = function(w, h) {
		width = w;
		height = h;

		init();
	};

	this.render = function(scene, camera) {
		renderer.render(scene, camera);
		asciify();
	};

	this.setup = function(settings) {
		// nice but slows down rendering!
		if (settings['color'] != undefined)
			bColor = settings['color'];

		// Transparency
		if (settings['alpha'] != undefined)
			bAlpha = settings['alpha'];

		// blocked characters. like good O dos
		if (settings['block'] != undefined)
			bBlock = settings['block'];

		// black is white, white is black
		if (settings['invert'] != undefined)
			bInvert = settings['invert'];

		// Some ASCII settings
		if (settings['scale'] != undefined)
			iScale = settings['scale'];

		// Resolution
		if (settings['strResolution'] != undefined) {
			strResolution = settings['strResolution'];
			switch (strResolution) {
			case "low":
				fResolution = 0.25;
				break;
			case "medium":
				fResolution = 0.5;
				break;
			case "high":
				fResolution = 1;
				break;
			}
		}

		// Adjust letter-spacing for all combinations of scale and resolution to
		// make it fit within the image width.
		// TODO: Unreliable; these are the settings for Courier only.
		if (settings['scale'] != undefined
				|| settings['strResolution'] != undefined) {
			if (strResolution == "low") {
				switch (iScale) {
				case 1:
					fLetterSpacing = -1;
					break;
				case 2:
				case 3:
					fLetterSpacing = -2.1;
					break;
				case 4:
					fLetterSpacing = -3.1;
					break;
				case 5:
					fLetterSpacing = -4.15;
					break;
				}
			} else if (strResolution == "medium") {
				switch (iScale) {
				case 1:
					fLetterSpacing = 0;
					break;
				case 2:
					fLetterSpacing = -1;
					break;
				case 3:
					fLetterSpacing = -1.04;
					break;
				case 4:
				case 5:
					fLetterSpacing = -2.1;
					break;
				}
			} else if (strResolution == "high") {
				switch (iScale) {
				case 1:
				case 2:
					fLetterSpacing = 0;
					break;
				case 3:
				case 4:
				case 5:
					fLetterSpacing = -1;
					break;
				}
			}
		}

		if (settings['fontColor'] != undefined
				&& tinycolor(settings['fontColor']).ok)
			fontColor = settings['fontColor'];
		if (settings['bgColor'] != undefined
				&& tinycolor(settings['bgColor']).ok)
			bgColor = settings['bgColor'];

		// Rendering style
		if (settings['renderStyle'] != undefined) {
			strAsciify = settings['renderStyle'];
			switch (strAsciify) {
			case "textual":
				init = initDOM;
				asciify = asciifyDOM;
				break;
			case "canvas":
				init = initCanvas;
				asciify = asciifyCanvas;
				break;
			case "no_effect":
				init = function() {
					domElement = oContainer;
					self.domElement = domElement;

					// Remove all existing children.
					while (domElement.firstChild) {
						domElement.removeChild(domElement.firstChild);
					}

					domElement.appendChild(oOriginalCanvas);

					oOriginalCanvas.width = width;
					oOriginalCanvas.height = height;

					// Set it full-pixel
					renderer.setSize(width, height);
				};
				asciify = function() {
				};
				break;
			}
		}

		init();
	};

	// AsciiEffect content container
	// The content can be switched to oCanvas or oAscii.
	var oContainer = document.createElement('div');

	var oCanvas = document.createElement('canvas');
	if (oCanvas == undefined || oCanvas.getContext == undefined) {
		console.log('AsciiEffect: error: failed to prepare a Canvas element');
		return;
	}
	var oCtx = oCanvas.getContext('2d');
	if (oCtx == undefined || oCtx.getImageData == undefined) {
		console
				.log('AsciiEffect: error: CanvasRenderingContext2D.getImageData not defined');
		return;
	}
	// var aWebGLCtxID = [ "webgl", "experimental-webgl", "webkit-3d",
	// "moz-webgl"
	// ];
	// for (var i = 0; i < aWebGLCtxID.length; ++i) {
	// if (oOriginalCanvas.supportsContext(aWebGLCtxID[i])) {
	// oCtx = oOriginalCanvas.getContext(aWebGLCtxID[i], {
	// antialias : false,
	// preserveDrawingBuffer : true
	// });
	// if (oCtx == undefined || oCtx.readPixels == undefined) {
	// console.log('WebGLRenderingContext.readPixels not defined');
	// return;
	// }
	// break;
	// }
	// }
	// if (oCtx == undefined && oOriginalCanvas.supportsContext("2d")) {
	// oCtx = oOriginalCanvas.getContext("2d");
	// if (oCtx == undefined || oCtx.getImageData == undefined) {
	// console.log('CanvasRenderingContext2D.getImageData not defined');
	// return;
	// }
	// }
	// if (oCtx == undefined) {
	// console.log('Failed to create canvas context');
	// return;
	// }

	var calcBrightness = function(r, g, b) {
		return (0.3 * r + 0.59 * g + 0.11 * b) / 255;
	};

	// Setup DOM
	// can't get a span or div to flow like an img element, but a table works?
	var oAscii;
	function initDOM() {
		domElement = oContainer;
		self.domElement = domElement;

		// Remove all existing children.
		while (domElement.firstChild) {
			domElement.removeChild(domElement.firstChild);
		}

		oAscii = document.createElement("table");
		domElement.appendChild(oAscii);

		iWidth = Math.ceil(width / fFontWidth);
		iHeight = Math.ceil(height / fFontHeight);

		oCanvas.width = iWidth;
		oCanvas.height = iHeight;

		// No need of excess resolution for the original canvas and its
		// renderer.
		renderer.setSize(iWidth, iHeight);

		if (parent != undefined && oOriginalCanvas.style.backgroundColor) {
			oAscii.rows[0].cells[0].style.backgroundColor = oOriginalCanvas.style.backgroundColor;
			oAscii.rows[0].cells[0].style.color = oOriginalCanvas.style.color;
		}

		oAscii.cellSpacing = 0;
		oAscii.cellPadding = 0;

		var oStyle = oAscii.style;
		oStyle.display = "inline-block";
		oStyle.position = "relative";
		oStyle.verticalAalign = "middle";
		oStyle.width = Math.ceil(iWidth * fFontWidth) + "px";
		oStyle.height = Math.ceil(iHeight * fFontHeight) + "px";
		oStyle.whiteSpace = "pre";
		oStyle.margin = "0px";
		oStyle.padding = "0px";
		oStyle.letterSpacing = fLetterSpacing + "px";
		oStyle.font = fFontHeight + "px " + strFont;
		oStyle.lineHeight = fFontHeight + "px";
		oStyle.textAlign = "left";
		oStyle.textDecoration = "none";
		oStyle.color = fontColor;
		oStyle.backgroundColor = bgColor;
	}

	// Convert the original rendering image to ASCII
	function asciifyDOM() {
		// First, render input image for ASCIIfication.
		/*
		 * NOTE: drawImage is affected by alpha, so that clearRect is
		 * required... Data copy with no alpha composition is better because
		 * clearRect won't be necessary.
		 */
		oCtx.clearRect(0, 0, iWidth, iHeight);
		oCtx.drawImage(oOriginalCanvas, 0, 0, iWidth, iHeight);
		var oImgData = oCtx.getImageData(0, 0, iWidth, iHeight).data;

		// Coloring loop starts now
		var strChars = "";

		// console.time('rendering');

		for (var y = 0; y < iHeight; ++y) {
			for (var x = 0; x < iWidth; ++x) {
				var iOffset = (y * iWidth + x) * 4;

				var iRed = oImgData[iOffset];
				var iGreen = oImgData[iOffset + 1];
				var iBlue = oImgData[iOffset + 2];
				var iAlpha = oImgData[iOffset + 3];
				var iCharIdx;

				var fBrightness = calcBrightness(iRed, iGreen, iBlue);

				iCharIdx = Math.floor((1 - fBrightness)
						* (aCharList.length - 1));

				if (bInvert) {
					iCharIdx = aCharList.length - iCharIdx - 1;
				}

				// good for debugging
				// fBrightness = Math.floor(fBrightness * 10);
				// strThisChar = fBrightness;

				var strThisChar = aCharList[iCharIdx];

				if (strThisChar == " ")
					strThisChar = "&nbsp;";

				if (bColor) {
					strChars += "<span style='"
							+ "color:rgb("
							+ iRed
							+ ","
							+ iGreen
							+ ","
							+ iBlue
							+ ");"
							+ (bBlock ? "background-color:rgb(" + iRed + ","
									+ iGreen + "," + iBlue + ");" : "")
							+ (bAlpha ? "opacity:" + (iAlpha / 255) + ";" : "")
							+ "'>" + strThisChar + "</span>";
				} else {
					strChars += strThisChar;
				}
			}
			strChars += "<br/>";
		}

		oAscii.innerHTML = "<tr><td>" + strChars + "</td></tr>";

		// console.timeEnd('rendering');
	}

	// end modified asciifyDOM block
	var getRgba;
	var coloring;
	var renderAscii;
	function initCanvas() {
		domElement = oContainer;
		self.domElement = domElement;

		// Remove all existing children.
		while (domElement.firstChild) {
			domElement.removeChild(domElement.firstChild);
		}

		domElement.appendChild(oCanvas);

		iWidth = Math.ceil(width / fFontWidth);
		iHeight = Math.ceil(height / fFontHeight);

		oCanvas.width = width;
		oCanvas.height = height;

		// No need of excess resolution for the original canvas and its
		// renderer.
		renderer.setSize(iWidth, iHeight);

		// Inherit certain styles from parent DOM element.
		// Undefined styles here are later set to default values for a canvas
		// context (depending on the context is of 2D, WebGL or else, default
		// values differ).
		/*
		 * if (parent != undefined) { if (parent.currentStyle != undefined) { if
		 * (parent.currentStyle['color'] != undefined) { //
		 * oCanvas.style.backgroundColor = //
		 * oOriginalCanvas.style.backgroundColor; oCanvas.style.backgroundColor =
		 * 'gray'; oCtx.fillStyle = oCtx.strokeStyle =
		 * parent.currentStyle['color']; } } else if (document.defaultView) { if
		 * (styles['color'] != undefined) { // oCanvas.style.backgroundColor = //
		 * oOriginalCanvas.style.backgroundColor; oCanvas.style.backgroundColor =
		 * 'gray'; oCtx.fillStyle = oCtx.strokeStyle = styles['color']; } } }
		 */
		oCanvas.style.backgroundColor = bgColor;
		oCtx.fillStyle = oCtx.strokeStyle = fontColor;

		if (bInvert) {
			getRgba = function(oImgData, iOffset) {
				return {
					iRed : 255 - oImgData[iOffset],
					iGreen : 255 - oImgData[iOffset + 1],
					iBlue : 255 - oImgData[iOffset + 2],
					iAlpha : oImgData[iOffset + 3]
				};
			};
		} else {
			getRgba = function(oImgData, iOffset) {
				return {
					iRed : oImgData[iOffset],
					iGreen : oImgData[iOffset + 1],
					iBlue : oImgData[iOffset + 2],
					iAlpha : oImgData[iOffset + 3]
				};
			};
		}

		// Coloring function
		if (bColor) {
			if (bAlpha) {
				coloring = function(r, g, b, a) {
					var strColor = 'rgba(' + r + ',' + g + ',' + b + ','
							+ (a / 255) + ')';
					oCtx.fillStyle = strColor;
					return calcBrightness(r, g, b);
				};
			} else {
				coloring = function(r, g, b, a) {
					var strColor = 'rgb(' + r + ',' + g + ',' + b + ')';
					oCtx.fillStyle = strColor;
					return calcBrightness(r, g, b);
				};
			}
		} else if (bBlock) {
			if (bAlpha) {
				coloring = function(r, g, b, a) {
					var fBrightness = calcBrightness(r, g, b);
					var tmpColor = tinycolor('hsl(0, 0%, '
							+ (fBrightness * 100) + '%)');
					tmpColor.setAlpha(a / 255);
					oCtx.fillStyle = tmpColor.toRgbString();
					return fBrightness;
				};
			} else {
				coloring = function(r, g, b, a) {
					var fBrightness = calcBrightness(r, g, b);
					oCtx.fillStyle = 'hsl(0, 0%, ' + (fBrightness * 100) + '%)';
					return fBrightness;
				};
			}
		} else {
			if (bAlpha) {
				coloring = function(r, g, b, a) {
					var tmpColor = tinycolor(fontColor);
					tmpColor.setAlpha(a / 255);
					oCtx.fillStyle = tmpColor.toRgbString();
					var fBrightness = calcBrightness(r, g, b);
					return fBrightness;
				};
			} else {
				coloring = function(r, g, b, a) {
					oCtx.fillStyle = fontColor;
					var fBrightness = calcBrightness(r, g, b);
					return fBrightness;
				};
			}
		}

		if (bBlock) {
			renderAscii = function(str, x, y) {
				oCtx.fillRect(x * fFontWidth, y * fFontHeight, fFontWidth,
						fFontHeight);
			};
		} else {
			renderAscii = function(str, x, y) {
				// aCharCtx.push(str)
				// aCharCtxList.push(aCharCtx);
				oCtx.fillText(str, x * fFontWidth, y * fFontHeight, fFontWidth);
			};
		}

		oCtx.font = fFontHeight + "px " + strFont;
		// oStyle.letterSpacing = fLetterSpacing + "px";
		// oStyle.lineHeight = fFontHeight + "px";
		// oStyle.textAlign = "left";
		// oStyle.textDecoration = "none";
	}

	// ASCIIfy, implemented with canvas element
	// TODO: Should (1) and (2) be performed in different buffers (or
	// canvases)? A matter of optimization.
	// TODO: Use of Typed Array in pixel processing can be a boost.
	// TODO: If something like a display list in OpenGL is available, that's
	// should be used; client-to-server or (client-to-software) data/command
	// transactions can be reduced. With non-client side reusable rendering
	// context, rendering operation, presumably for monochrome rendering where
	// bColor == false, can be fast.
	function asciifyCanvas() {
		// (1) render input image for ASCIIfication.
		/*
		 * NOTE: drawImage is affected by alpha, so that clearRect is
		 * required... Data copy with no alpha composition is better because
		 * clearRect won't be necessary.
		 */
		oCtx.clearRect(0, 0, iWidth, iHeight);
		oCtx.drawImage(oOriginalCanvas, 0, 0, iWidth, iHeight);
		var oImgData = oCtx.getImageData(0, 0, iWidth, iHeight).data;

		// # WebGL #
		// //void readPixels(GLint x, GLint y, GLsizei width, GLsizei height,
		// //GLenum format, GLenum type, ArrayBufferView? pixels)
		// buf = new Uint8Array(iWidth * iHeight * 4);
		// oCtx.readPixels(0, 0, iWidth, iHeight, oCtx.RGBA, oCtx.UNSIGNED_BYTE,
		// buf);

		oCtx.clearRect(0, 0, width, height);

		// console.time('rendering');

		// (2) render ASCIIfied image
		// TODO: Results of for block below are independent from the others;
		// thus they can be run in separate threads. This implies ASCIIfication
		// for canvas can be implemented in GLSL of WebGL.
		for (var y = 0; y < iHeight; ++y) {
			// Coloring loop starts now
			// var aCharCtxList = new Array(iWidth);
			for (var x = 0; x < iWidth; ++x) {
				// var aCharCtx = [];

				var iOffset = (y * iWidth + x) * 4;

				var mapRgba = getRgba(oImgData, iOffset);

				var fBrightness = coloring(mapRgba['iRed'], mapRgba['iGreen'],
						mapRgba['iBlue'], mapRgba['iAlpha']);

				// Brightness -> Char index
				// Bright = sparse char
				var iCharIdx = Math.floor((1 - fBrightness)
						* (aCharList.length - 1));

				// good for debugging
				// fBrightness = Math.floor(fBrightness * 10);
				// strThisChar = fBrightness;

				var strThisChar = aCharList[iCharIdx];

				renderAscii(strThisChar, x, y);
			}
			// for (var i = 0; i < iWidth; ++i) {
			// // aCharCtxList
			// // Should I backup fillStyle?
			// // oCtx.fillStyle = strColor;
			// // oCtx.fillText(strThisChar, x * fFontWidth, y * fFontHeight /
			// 2,
			// // fFontWidth);
			// }
		}
	}
};
